package com.googlecode.yoohoo.xmppcore.protocol.parsing;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import com.googlecode.yoohoo.utils.Utils;
import com.googlecode.yoohoo.xmppcore.protocol.XmppProtocolException;
import com.googlecode.yoohoo.xmppcore.protocol.parsing.error.IStanzaErrorParser;
import com.googlecode.yoohoo.xmppcore.protocol.parsing.error.StanzaErrorParser;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.Stanza;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.error.BadRequest;
import com.googlecode.yoohoo.xmppcore.protocol.stanza.error.StanzaError;
import com.googlecode.yoohoo.xmppcore.protocol.stream.error.BadFormat;
import com.googlecode.yoohoo.xmppcore.utils.XmlUtils;

public class XmppParsingFactory implements IXmppParsingFactory {	
	private Map<ProtocolKey, IXmppParserFactory<?>> parserFactories;
	private Set<ProtocolRelation> protocolRelations;
	private Map<ProtocolKey, ProtocolRelations> protocolKeyAndRelations;
	private XMLInputFactory xmlInputFactory;
	
	private ReadWriteLock lock = new ReentrantReadWriteLock();
	private Lock updateFactoryLock = lock.writeLock();
	private Lock parsingLock = lock.readLock();
	
	public XmppParsingFactory() {
		parserFactories = new HashMap<ProtocolKey, IXmppParserFactory<?>>();
		protocolRelations = new HashSet<ProtocolRelation>();
		protocolKeyAndRelations = new HashMap<ProtocolKey, ProtocolRelations>();
		xmlInputFactory = XMLInputFactory.newInstance();
		xmlInputFactory.setProperty("javax.xml.stream.isCoalescing", true);
		xmlInputFactory.setProperty("javax.xml.stream.isValidating", false);
	}

	@Override
	public Object parse(String xml) {
		return parse(xml, false);
	}
	
	@Override
	public Object parse(String xml, boolean stream) {
		try {
			parsingLock.lock();
			return doParse(xml);
		} catch (XMLStreamException e) {
			if (stream) {
				throw new XmppProtocolException(new BadFormat("XML parsing error. Not a valid xml."));
			} else {
				throw new XmppProtocolException(new BadRequest("XML parsing error. Not a valid xml."));
			}			
		} finally {
			parsingLock.unlock();
		}
	}
	
	private Object doParse(String xml) throws XMLStreamException {
		XMLStreamReader reader = null;
		try {
			StringBuilder builder = new StringBuilder();
			builder.append("<stream:stream xmlns:stream='http://etherx.jabber.org/streams'>");
			builder.append(xml);
			builder.append("</stream:stream>");
			xml = builder.toString();
			
			reader = xmlInputFactory.createXMLStreamReader(new StringReader(xml));	
			reader.next();
			
			int eventType = reader.nextTag();
			if (eventType != XMLStreamConstants.START_ELEMENT) {
				throw new XmppProtocolException(new BadRequest("Not valid xml. No start element."));
			}
			
			Object obj = parseProtocol(reader, xml, null);
			
			return obj;
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (XMLStreamException e) {
					// ignore
				}
			}
		}
	}

	private Object parseProtocol(XMLStreamReader reader, String xml, ProtocolKey enclosing) throws XMLStreamException {
		String localName = reader.getLocalName();
		String namespace = reader.getNamespaceURI();
		
		IXmppParserFactory<?> parserFactory = parserFactories.get(new ProtocolKey(localName, namespace));
		
		if (parserFactory == null) {
			parserFactory = parserFactories.get(new ProtocolKey(namespace));
		}
		
		if (parserFactory == null) {
			throw new XmppProtocolException(new BadRequest(String.format("Can't find parser. Unsupported protocol[%s, %s].",
					localName, namespace)));
		}
		
		ProtocolRelation relation = new ProtocolRelation(enclosing, parserFactory.getProtocolKey());
		if (!protocolRelations.contains(relation)) {
			if (enclosing == null) {
				throw new XmppProtocolException(new BadRequest(String.format("XML parsing error. Unsupported top protocol[%s, %s].", localName, namespace)));
			} else {
				throw new XmppProtocolException(new BadRequest(String.format("XML parsing error. Unsupported embedded protocol[%s, %s]/[%s, %s].",
						enclosing == null ? "null" : enclosing.getLocalName(), enclosing == null ? "null" : enclosing.getNamespace(),
								localName, namespace)));
			}
		}
		
		ParsingHierarchy hierarchy = new ParsingHierarchy(localName);
		IXmppParser<?> parser = parserFactory.createParser();
		List<Attribute> attributes = readAttributes(reader);
		parser.processAttributes(hierarchy.toString(), attributes);
		
		// stanza errors are so special. they need to be processed separately.
		if (enclosing == null) {
			Object obj = parser.getObject();
			if (obj instanceof Stanza) {
				Stanza stanza = (Stanza)obj;
				if (StanzaErrorParser.ERROR_TYPE_STRING.equals(stanza.getType())
						&& (!(stanza instanceof StanzaError))) {
					return parseStanzaError(reader, xml, stanza);
				}
			}
		}
		
		StringBuilder text = null;
		while (true) {
			int eventType = reader.next();
			if (eventType == XMLStreamConstants.CHARACTERS) {
				text = ParsingUtils.appendText(reader, text);
			} else if (eventType == XMLStreamConstants.END_ELEMENT) {
				parser.processText(hierarchy.toString(), (text == null ? null : XmlUtils.unescape(text.toString())));
				break;
			} else if (eventType == XMLStreamConstants.START_ELEMENT) {
				parseChildElement(reader, xml, localName, namespace, hierarchy, parser);
			} else {
				throw new XmppProtocolException(new BadRequest("Unspported XML event type."));
			}
		}
		
		return parser.getObject();
	}

	protected Object parseStanzaError(XMLStreamReader reader, String xml, Stanza stanza) throws XMLStreamException {
		IStanzaErrorParser parser = new StanzaErrorParser();
		return parser.parse(this, reader, xml, stanza);
	}

	private void parseChildElement(XMLStreamReader reader, String xml, String name, String namespace,
				ParsingHierarchy hierarchy, IXmppParser<?> parser) throws XMLStreamException {
		String childElementLocalName = reader.getLocalName();
		String childElementNamespace = reader.getNamespaceURI();
		
		if (!Utils.equalsEvenNull(namespace, childElementNamespace)) {
			ProtocolKey enclosingPk = new ProtocolKey(name, namespace);
			ProtocolKey embeddedPk = new ProtocolKey(childElementLocalName, childElementNamespace);
			parser.processEmbeddedObject(embeddedPk, parseProtocol(reader, xml, enclosingPk));
		} else {
			ParsingHierarchy childHierarchy = new ParsingHierarchy(childElementLocalName);
			childHierarchy.setParent(hierarchy);					
			parseElement(reader, xml, childHierarchy, namespace, parser);
		}
	}

	private List<Attribute> readAttributes(XMLStreamReader reader) {
		int attributeCount = reader.getAttributeCount();
		if (attributeCount == 0)
			return new ArrayList<Attribute>(0);
		
		List<Attribute> attributes = new ArrayList<Attribute>();
		for (int i = 0; i < attributeCount; i++) {
			String prefix = reader.getAttributePrefix(i);
			if ("".equals(prefix))
				prefix = null;
			attributes.add(new Attribute(prefix, reader.getAttributeLocalName(i),
					reader.getAttributeValue(i)));
		}
		
		return attributes;
	}

	private void parseElement(XMLStreamReader reader, String xml, ParsingHierarchy hierarchy, String currentNamespace,
			IXmppParser<?> parser) throws XMLStreamException {
		parser.processAttributes(hierarchy.toString(), readAttributes(reader));
		
		StringBuilder text = null;
		while (true) {
			int eventType = reader.next();
			if (eventType == XMLStreamConstants.CHARACTERS) {
				text = ParsingUtils.appendText(reader, text);
			}else if (eventType == XMLStreamConstants.END_ELEMENT) {
				parser.processText(hierarchy.toString(), (text == null ? null : XmlUtils.unescape(text.toString())));
				return;
			} else if (eventType == XMLStreamConstants.START_ELEMENT) {
				parseChildElement(reader, xml, hierarchy.getLocalName(), currentNamespace, hierarchy, parser);
			} else {
				throw new XmppProtocolException(new BadRequest("Unspported XML event type."));
			}
		}
	}

	@Override
	public void registerParser(IXmppParserFactory<?> parserFactory,
			ProtocolRelations relations) {
		try {
			updateFactoryLock.lock();
			doRegisterParser(parserFactory, relations);
		} finally {
			updateFactoryLock.unlock();
		}
	}

	private void doRegisterParser(IXmppParserFactory<?> parserFactory,
			ProtocolRelations relations) {
		if (parserFactory == null) {
			throw new IllegalArgumentException("Null parser factory.");
		}
		
		parserFactories.put(parserFactory.getProtocolKey(), parserFactory);
		if (relations != null) {
			protocolKeyAndRelations.put(parserFactory.getProtocolKey(), relations);
			for (ProtocolRelation relation : relations.getProtocolRelations()) {
				protocolRelations.add(relation);
			}
		}
	}

	@Override
	public void unregisterParser(IXmppParserFactory<?> parserFactory) {
		try {
			updateFactoryLock.lock();
			doUnregisterParser(parserFactory);
		} finally {
			updateFactoryLock.unlock();
		}
	}

	private void doUnregisterParser(IXmppParserFactory<?> parserFactory) {
		if (parserFactory == null) {
			throw new IllegalArgumentException("Null parser factory.");
		}
		
		ProtocolRelations relations = protocolKeyAndRelations.get(parserFactory.getProtocolKey());
		for (ProtocolRelation relation : relations.getProtocolRelations()) {
			protocolRelations.remove(relation);
		}
		protocolKeyAndRelations.remove(parserFactory.getProtocolKey());
		parserFactories.remove(parserFactory.getProtocolKey());
	}
	
	private class ParsingHierarchy {
		private boolean changed;
		private ParsingHierarchy parent;
		private String localName;
		private String path;

		public ParsingHierarchy(String name) {
			this(name, null);
		}
		
		public ParsingHierarchy(String name, ParsingHierarchy parent) {
			this.localName = name;
			this.parent = parent;
			this.changed = false;
		}
		
		public void setParent(ParsingHierarchy parent) {
			this.parent = parent;
			changed = true;
		}
		
		public String getLocalName() {
			return localName;
		}
		
		@Override
		public String toString() {
			if (changed || path == null) {
				StringBuilder builder = new StringBuilder();
				if (parent != null) {
					builder.append(parent);
					builder.append('/');
				}
				
				builder.append(localName);
				path = builder.toString();
			}
			
			return path;
		}
		
	}

	@Override
	public void registerParser(IXmppParserFactory<?> parserFactory,
				ProtocolRelation relation) {
		ProtocolRelations relations = new ProtocolRelations();
		relations.addProtocolRelation(relation);
		
		registerParser(parserFactory, relations);
	}	
}
